package test.gwt.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import test.gwt.client.MyWebApp.MPen.Direction;
import test.gwt.client.MyWebApp.PlayField.CellContent;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.CssColor;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.user.client.Random;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;

/**
 * If the only direction available is the opposite then change dir. Allow backup if stuck.
 */
public class MyWebApp implements EntryPoint { 

	private static final int MODEL_TO_SCREEN_MULT = 8;

	static final String holderId = "canvasholder";

	private Canvas canvas;
	private Canvas backBuffer;
	private Context2d context;
	private PlayField play;
	private MPen pen;

	private int cntSinceLastDirChange = 0;

	private int playFieldWidth = 50;
	private int playFieldHeight = 50;

	private static boolean moduleLoaded = false;

	/**
	 * This is the entry point method.
	 */
	public void onModuleLoad() {
		// backBuffer = Canvas.createIfSupported();

		RootPanel rootPanel = RootPanel.get(holderId);
		rootPanel.getElement().getStyle().setPosition(Position.RELATIVE);

		DockPanel dockPanel = new DockPanel();
		rootPanel.add(dockPanel);

		FlowPanel flowPanel = new FlowPanel();
		dockPanel.add(flowPanel, DockPanel.NORTH);
		flowPanel.setSize("666px", "50px");

		canvas = Canvas.createIfSupported();
		dockPanel.add(canvas, DockPanel.CENTER);

		canvas.setWidth(400 + "px");
		canvas.setHeight(400 + "px");
		canvas.setCoordinateSpaceWidth(400);
		canvas.setCoordinateSpaceHeight(400);

		if (canvas == null) {
			RootPanel.get().add(new Label("Sorry, your browser doesn't support the HTML5 Canvas element"));
			return;
		}
		context = canvas.getContext2d();
		initialise();

		moduleLoaded = true;

		final Timer timer = new Timer() {
			@Override
			public void run() {
				drawSomethingNew();
			}
		};
		timer.scheduleRepeating(5);
	}

	private void initialise() {
		play = new PlayField(playFieldWidth, playFieldHeight);
		pen = new MPen(play, playFieldWidth / 2, playFieldHeight / 2, Direction.anticlockwise);

		context.beginPath();
		context.moveTo(pen.getX() * MODEL_TO_SCREEN_MULT, pen.getY() * MODEL_TO_SCREEN_MULT);
		context.setLineWidth(1);
		CssColor colour = CssColor.make(255, 0, 0);
		context.setStrokeStyle(colour);

		pen.updatePosition(pen.getX() + 1, pen.getY());
		draw();

		colour = CssColor.make(0, 0, 0);
		context.setStrokeStyle(colour);
	}

	public void drawSomethingNew() {

		// TODO: Check if we have covered the entire area.
		boolean moreToDo = true;
		
		// give it a one in 10 chance of changing direction
		if (cntSinceLastDirChange > 5 && Random.nextInt(25) == 1) {
			cntSinceLastDirChange = 0;
			pen.setDirection(pen.getDirection() == Direction.anticlockwise ? Direction.clockwise : Direction.anticlockwise);
		}
		else {
			cntSinceLastDirChange++;
		}

		if (!pen.moveToNextValidPosition()) {
			boolean canBackup = pen.backup();	//We've backed into a dead end. Might be the start or another dead end where we backed from earlier.			

			if (!canBackup)	{
				//TODO: Now is probably the time to check the grid to see if it is full.
				//pen.reverse();
				moreToDo = pen.relocateToEmptyPosition();
			}
		}
		
		
		draw();

	}

	private void draw() {
		context.moveTo(pen.getPreviousx() * MODEL_TO_SCREEN_MULT, pen.getPreviousy() * MODEL_TO_SCREEN_MULT);
		context.lineTo(pen.getX() * MODEL_TO_SCREEN_MULT, pen.getY() * MODEL_TO_SCREEN_MULT);
		context.stroke();
	}

	public static class MPen {
		public enum Direction {
			clockwise, anticlockwise
		};

		private PlayField field;
		private int x;
		private int y;
		private int previousx;
		private int previousy;
		private Direction direction;

		public MPen(PlayField field, int x, int y, Direction direction) {
			this.field = field;
			this.x = x;
			this.y = y;
			this.previousx = x;
			this.previousy = y;
			this.direction = direction;
			field.set(x, y, CellContent.wall);
		}

		/**
		 * Iterate over the field looking for a new place to start. Could do this by walking backwards but this is faster
		 * as the field becomes more crowded.
		 * @return
		 */
		public boolean relocateToEmptyPosition() {
			//loop through looking for a wall
			Position p = null;
			for (int y = 0; y < field.getHeight(); y++) {
				for (int x = 0; x < field.getWidth(); x++) {
					CellContent cellContent = field.get(x, y);
					if (cellContent==CellContent.wall){
						p = new Position(x, y);

						//pick a random wall next to it for the previous
						Position previous = null;
						previous = previous==null ? isPositionOfType(p.getX()-1, p.getY(), CellContent.wall) : null;
						previous = previous==null ? isPositionOfType(p.getX(), p.getY()-1, CellContent.wall) : null;
						previous = previous==null ? isPositionOfType(p.getX()+1, p.getY(), CellContent.wall) : null;
						previous = previous==null ? isPositionOfType(p.getX(), p.getY()+1, CellContent.wall) : null;
						
						//try to move. If possible then keep else next
						this.setPosition(p);
						this.setPreviousPosition(previous);
						
						if (moveToNextValidPosition())	{
							return true;
						}
					}
				}
			}			
			
			
			return false;
		}

		private Position isPositionOfType(int xToTest, int yToTest, CellContent content) {
			if (field.get(xToTest, yToTest)==content)	{
				return new Position(xToTest, yToTest);
			}
			return null;
		}

		public void reverse() {
			int px = x;
			int py = y;
			x = previousx;
			y = previousy;
			previousx = px;
			previousy = py;
		}

		public MPen(PlayField field, int x, int y, int previousx, int previousy, Direction direction) {
			this(field, x, y, direction);
			this.previousx = previousx;
			this.previousy = previousy;
			field.set(previousx, previousy, CellContent.wall);
		}

		/**
		 * Move back along the line already drawn. It is possible for there to be a cross if we have already backed up so choose randomly.
		 */
		public boolean backup() {
			// the new 'previous' position will be the only line that is not the current previous.
			List<Position> possiblePosition = new ArrayList<MyWebApp.MPen.Position>(1);

			testBackMove(possiblePosition, previousx - 1, previousy); // Start to the left
			testBackMove(possiblePosition, previousx, previousy - 1); // top
			testBackMove(possiblePosition, previousx + 1, previousy); // right
			testBackMove(possiblePosition, previousx, previousy + 1); // below

			// If we didn't find one we're back at the start with nothing left to do
			int bound = possiblePosition.size();
			if (bound == 0) {
				return false;
			}

			int chosen = getRandom(bound);
			this.x = previousx; // the new position is the previous one.
			this.y = previousy;
			this.setPreviousPosition(possiblePosition.get(chosen));

			return true;

		}

		private int getRandom(int bound) {
			return moduleLoaded ? Random.nextInt(bound) : ((int) (Math.floor(Math.random() * bound)));
		}

		private void testBackMove(List<Position> possiblePosition, int xpos, int ypos) {
			if (field.get(xpos, ypos) == CellContent.wall && !(xpos == x && ypos == y)) {
				possiblePosition.add(new Position(xpos, ypos));
			}
		}

		public void setPosition(Position position) {
			this.x = position.getX();
			this.y = position.getY();
		}

		public void setPreviousPosition(Position position) {
			this.previousx = position.getX();
			this.previousy = position.getY();
		}

		/**
		 * Pick the next position moving in the direction of the pen.
		 * 
		 * @return
		 */
		public boolean moveToNextValidPosition() {
			// We need to know the direction we were moving in for a context of anti vs clockwise
			int xmove = x - previousx;
			int ymove = y - previousy;

			// anticlockwise
			if (direction == Direction.anticlockwise) {
				if (!moveIfValid(x + ymove, y - xmove)) { // left
					if (!moveIfValid(x + xmove, y + ymove)) { // forward
						if (!moveIfValid(x - ymove, y + xmove)) { // right
							return false; // non worked so return false
						}
					}
				}
			}
			else {
				if (!moveIfValid(x - ymove, y + xmove)) { // right
					if (!moveIfValid(x + xmove, y + ymove)) { // forward
						if (!moveIfValid(x + ymove, y - xmove)) { // left
							return false; // non worked so return false
						}
					}
				}
			}

			return true;

		}

		private boolean moveIfValid(int x, int y) {
			if (validMove(x, y)) {
				updatePosition(x, y);
				return true;
			}
			return false;
		}

		public void updatePosition(int newX, int newY) {
			this.previousx = x;
			this.previousy = y;
			x = newX;
			y = newY;
			field.set(newX, newY, PlayField.CellContent.wall);
		}

		// Can only move up, down, left, right
		// Check all surrounding cells. They should all be empty apart from the current location;
		// Check the cell in 'front' and two the sides of that.
		public boolean validMove(int newX, int newY) {
			// can't move to where there is already a wall
			if (field.get(newX, newY) == CellContent.wall) {
				return false;
			}

			int xdiff = newX - x;
			int ydiff = newY - y;

			// No diagonal moves
			if (xdiff != 0 && ydiff != 0) {
				return false;
			}

			int frontX = newX + xdiff;
			int frontY = newY + ydiff;

			// A wall in front so blocking a path.
			if (field.get(frontX, frontY).isBlocking()) {
				return false;
			}

			// the sides of the front.
			int leftFX = frontX + ydiff;
			int leftFY = frontY + xdiff;
			int rightFX = frontX - ydiff;
			int rightFY = frontY - xdiff;

			if (field.get(leftFX, leftFY) == PlayField.CellContent.wall) {
				return false;
			}
			if (field.get(rightFX, rightFY) == PlayField.CellContent.wall) {
				return false;
			}

			// To the sides of the actual
			int leftX = newX + ydiff;
			int leftY = newY + xdiff;
			int rightX = newX - ydiff;
			int rightY = newY - xdiff;

			if (field.get(leftX, leftY) == PlayField.CellContent.wall) {
				return false;
			}
			if (field.get(rightX, rightY) == PlayField.CellContent.wall) {
				return false;
			}

			return true;
		}

		public Direction getDirection() {
			return direction;
		}

		public void setDirection(Direction direction) {
			this.direction = direction;
		}

		public int getX() {
			return x;
		}

		public int getY() {
			return y;
		}

		public int getPreviousx() {
			return previousx;
		}

		public int getPreviousy() {
			return previousy;
		}

		public static class Position {
			private int x;
			private int y;

			private Position(int x, int y) {
				this.x = x;
				this.y = y;
			}

			public int getX() {
				return x;
			}

			public void setX(int x) {
				this.x = x;
			}

			public int getY() {
				return y;
			}

			public void setY(int y) {
				this.y = y;
			}

		}
	}

	public static class PlayField {
		public enum CellContent {
			path(false), wall(true), outside(true);

			private final boolean blocking;

			private CellContent(boolean blocking) {
				this.blocking = blocking;
			}

			public boolean isBlocking() {
				return blocking;
			}

			@Override
			public String toString() {
				return isBlocking() ? "+" : ".";
			}
		};

		private CellContent[][] field;

		public PlayField(int width, int height) {
			field = new CellContent[width][height];
			clearCells();
		}

		public void set(int x, int y, CellContent c) {
			field[x][y] = c;
		}

		public CellContent get(int x, int y) {
			if (x < 0 || x > field.length - 1 || y < 0 || y > field[x].length - 1) {
				return CellContent.outside;
			}
			return field[x][y];
		}

		@Override
		public String toString() {
			String output = "";
			for (int y = 0; y < field.length; y++) {
				for (int x = 0; x < field[y].length; x++) {
					output += get(x, y).toString();
				}
				output += "\r\n";
			}
			return output;
		}

		public void clearCells() {
			for (CellContent[] c : field) {
				Arrays.fill(c, CellContent.path);
			}
		}
		
		public int getWidth()	{
			return field.length;
		}
		
		public int getHeight()	{
			return field[0].length;
		}

	}

}
